Skip to content

Add admin-editable alternative search terms per mod, searchable on /admin/ranked-mods - #52

Merged
V-rtualized merged 3 commits into
mqttfrom
feat/mod-alternative-search-terms
Sep 13, 2026
Merged

V-rtualized merged 3 commits into
mqttfrom
feat/mod-alternative-search-terms

Conversation

@12problems

Copy link
Copy Markdown

Problem

Admins managing the ranked mod catalog had no way to find a mod by anything other than its real title/id - e.g. "What's in my Fool" is commonly called "wimf" by players, but there was no way to associate that alias with the mod, and no search box on /admin/ranked-mods at all.

Changes

  • mod_registry gets a new search_terms text[] column (migration 0036_mod_search_terms.sql), same shape as the existing categories column but with no upstream-index counterpart at all: unlike categories/title/author/etc (SYNCABLE_MOD_FIELDS), this is never touched by the hourly upstream sync and never participates in overriddenFields - there's no upstream value for it to ever protect. updateModFields() writes it directly instead of through the touch()/overriddenFields path the ten real syncable fields use.
  • Editable via the same admin field-edit form as categories (PATCH /webadmin/mods/:modId, and at creation via POST /webadmin/mods / PUT .../custom) - a comma-separated "Alternative search terms" input right below Categories in the mod dialog.
  • /admin/ranked-mods now has an actual search box (there wasn't one at all before) that filters the already-loaded catalog client-side by name, id, or any searchTerms entry, case-insensitive substring on each - the catalog is small enough this needs no server round trip per keystroke. The table's empty-state message now distinguishes "no mods synced yet" from "no mods match this search".

Verification

  • tsc --noEmit on both apps/server and apps/web shows no errors introduced by this change (every remaining error is a pre-existing missing-module gap in this checkout, e.g. @bmp/types/sanitize-html not installed, unrelated to any file this touches).
  • biome check flagged only pre-existing formatting/line-ending debt already present on these files before this change (confirmed via git stash comparison), nothing new.
  • Not yet run against a live database - the migration needs to be applied (pnpm migrate) before this deploys.

Follow-up

This only covers the admin website. The launcher (BET) doesn't yet consume searchTerms for its own in-app mod search - separate PR if wanted.

🤖 Generated with Claude Code

12problems and others added 3 commits September 13, 2026 14:12
…branch tip

skyline69/balatro-mod-index's own update_mod_versions.py bumps a
branch-tracked mod's `version` field to the latest commit SHA of the
*whole repo* on any commit anywhere in it, but only ever rewrites
`downloadURL` for its tag/release cases -- never for the HEAD case. So
every version ever recorded for such a mod carries the exact same
branch-archive URL, and downloading it always fetches "whatever's on
the branch right now", never the specific commit the version label
names. Confirmed live: Aikoyori@Aikoyoris-Shenanigans' version history
has a dozen distinct commit-hash labels all sharing one identical
downloadUrl and (whenever the branch hadn't moved between two label
bumps) identical sha256.

The real cost isn't the duplication -- it's that an older label
becomes permanently unfetchable once the branch advances past it,
silently breaking any profile (a Ranked rankedVersion pin, or a user
manually pinning an older dropdown entry) sitting on it.

Add resolveCommitPinnedDownloadUrl() (custom-mod-version-check.service.ts,
alongside its existing GitHub-resolution helpers): when a version looks
like a git SHA and its downloadUrl classifies as 'branch', resolve the
SHA to its full 40-char form via one GitHub API call and rebuild a
codeload.github.com/.../zip/<full-sha> URL that stays fetchable forever,
regardless of where the branch moves next.

Wire it into mods-sync.service.ts via pinBranchVersionIfNew(), called
right before a (modId, version) pair is hashed and stored for the first
time -- both for upstream-index entries and for automaticVersionCheck
custom mods tracking a branch. A version that's already been hashed is
left alone unconditionally, so this costs one extra GitHub API call per
brand-new branch-tracked version, never per sync.

Known follow-up (not done here): existing mod_registry_versions rows
synced before this fix keep their stale branch URL -- only new versions
get pinned going forward. A one-off backfill (mirroring
recomputeAllModHashes's shape) would be needed to retroactively pin
already-stored branch-tracked versions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pinBranchVersionIfNew() (previous commit) only pins a version's downloadUrl
the first time it's synced -- every mod_registry_versions row written
before that fix exists is still sitting on its original moving-branch-tip
URL, with a sha256 computed against whatever that tip happened to be at
hash-time (not necessarily the exact commit its own version label names).

Add backfill-branch-pins.ts (pnpm backfill-branch-pins), a one-off script
in the same shape as the existing backfill-mod-hashes.ts: finds every
mod_registry_versions row whose downloadUrl still classifies as 'branch',
resolves a commit-pinned URL for it, and *re-downloads and re-hashes* it
against that pinned URL rather than trusting the existing stored hash --
the old hash could already be wrong for the commit it's about to be
pinned to, so only a fresh hash guarantees correctness.

Each row gets up to 3 resolve+hash attempts with a short backoff before
being marked via a new mod_registry_versions.pin_failed_at column
(migration 0036) and skipped on future runs -- so a genuinely dead
repo/branch/commit doesn't keep burning GitHub API calls on every
re-run. Pass --retry-failed to re-attempt previously-marked rows (e.g.
after a renamed repo or an expired rate limit).

New gateway functions in mods.gateway.ts: listVersionsWithDownloadUrl
(broad fetch, filtered/classified in TS same as listAllVersionsWithDownloadUrl),
applyBranchPin (writes the pinned URL + hash, clears pinFailedAt),
markVersionPinFailed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dmin/ranked-mods

Admins managing the ranked mod catalog had no way to find a mod by
anything other than its real title/id - e.g. "What's in my Fool" is
commonly called "wimf" by players, but typing that into a (previously
nonexistent) search box would have found nothing.

- mod_registry gets a new search_terms text[] column (migration
  0036_mod_search_terms.sql), same shape as the existing categories
  column but with no upstream-index counterpart at all: unlike
  categories/title/author/etc (SYNCABLE_MOD_FIELDS), this is never
  touched by the hourly upstream sync and never participates in
  overriddenFields - there's no upstream value for it to ever protect.
  updateModFields() writes it directly instead of through the
  touch()/overriddenFields path the ten real syncable fields use.
- Editable via the same admin field-edit form as categories (PATCH
  /webadmin/mods/:modId, and at creation via POST /webadmin/mods /
  PUT .../custom) - a comma-separated "Alternative search terms" input
  right below Categories in the mod dialog.
- /admin/ranked-mods now has an actual search box (there wasn't one at
  all before) that filters the already-loaded catalog client-side by
  name, id, or any searchTerms entry, case-insensitive substring on
  each - the catalog is small enough this needs no server round trip
  per keystroke. The table's empty-state message now distinguishes "no
  mods synced yet" from "no mods match this search".

Verified: tsc --noEmit on both apps/server and apps/web shows no errors
introduced by this change (every remaining error is a pre-existing
missing-module gap in this checkout, e.g. @bmp/types/sanitize-html not
installed, unrelated to any file this touches). biome check flagged
only pre-existing formatting/line-ending debt already present on these
files before this change (confirmed via git stash comparison), nothing
new.

Not yet run against a live database - the migration should be applied
(pnpm migrate) before this deploys.
@V-rtualized
V-rtualized force-pushed the feat/mod-alternative-search-terms branch from 7364101 to 1cfeb78 Compare September 13, 2026 20:16
@V-rtualized
V-rtualized merged commit 1d9fa69 into mqtt Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants